# GAMEACTION.PY - Game agent actions
# HIT3046 AI for GAMES
# ------- Assignment 2
# Authors: Mike Blackney, 0906123
#          Alex Bruce, 5409438

import time
import math

import io
import pos
import gob
import astar
import utility
import gamemap

from defaults import *

################
# GUARD actions.

# Patrol.  Looks for a good location to patrol to, then pathfinds there.
class PATROL (gob.ACTION):
    def Enter( self ):
        self.start = self.Location( )
        self.path = None
        # Patrolling follows the pattern: findpatroldest, goto, lookaround.
        # Pushed on the stack backwards:
        self.PushAction( LOOKAROUND( ) )
        self.PushAction( GOTO( ) )
        self.PushAction( FINDPATROLDEST( ) )
        # SET THE ACTION SPRITE
        self.parent.spr_action = self.parent.SetSprite( io.SPRITE( "retreating_state.gif", 0 ) )        
    def Leave( self ):
        # CLEAR THE ACTION SPRITE
        self.parent.spr_action = self.parent.ClearSprite( self.parent.spr_action )
    def Tick( self, currTime, deltaTime ):
        #print "patrol sec=" + str(self.parent.d_security) + " str=" + str(self.parent.d_strength )
        gob.ACTION.Tick( self, currTime, deltaTime )
        # If we're done patroling, start a new patrol:
        if len(self.actionList) == 0:
            self.Leave( )
            self.Enter( )
            # And reward the guard:
            self.parent.d_security += PATROL_SEC
            self.parent.d_strength += PATROL_STR
            return
    def Priority( self ):
        # What is the player needing now?
        security = self.parent.d_security
        strength = self.parent.d_strength
        # What will he need when we're done?
        security += PATROL_SEC
        strength += PATROL_STR
        # Discontentment is the hypotenuse of the others:
        discontent = math.hypot(security,strength)
        return security*security + strength*strength + discontent*discontent

# Heal.
class HEAL (gob.ACTION):
    def Enter( self ):
        self.start = self.Location( )
        self.path = None
        self.healthpoint = None
        # Patrolling follows the pattern: findhealthpoint, goto, usehealthpoint.
        # Pushed on the stack backwards:
        self.PushAction( USEHEALTHPOINT( ) )
        self.PushAction( GOTO( ) )
        self.PushAction( FINDHEALTHPOINT( ) )
        # SET THE ACTION SPRITE
        self.parent.spr_action = self.parent.SetSprite( io.SPRITE( "repairing_state.gif", 0 ) )        
    def Leave( self ):
        # CLEAR THE ACTION SPRITE
        self.parent.spr_action = self.parent.ClearSprite( self.parent.spr_action )
        self.ClearActions( )
    def Tick( self, currTime, deltaTime ):
        #print "patrol sec=" + str(self.parent.d_security) + " str=" + str(self.parent.d_strength )
        gob.ACTION.Tick( self, currTime, deltaTime )
        # If we're done patroling, start a new patrol:
        if len(self.actionList) == 0:
            self.Leave( )
            self.Enter( )
            # Don't reward the guard -- usehealthpoint does that.
            return
    def Priority( self ):
        # What is the player needing now?
        security = self.parent.d_security
        strength = self.parent.d_strength
        # What will he need when we're done?
        security += HEAL_SEC
        strength += HEAL_STR
        # Discontentment is the hypotenuse of the others:
        discontent = math.hypot(security,strength)
        return security*security + strength*strength + discontent*discontent

# Engage.
class ENGAGE (gob.ACTION):
    def __init__( self, *args ):
        # Set up the player pointer so that priority can check for
        # a player before we've ever engaged the player:
        self.spy = None
        self.player_pos = None
        # Chain up:
        gob.ACTION.__init__( self, args )
    def Enter( self ):
        # Setup a location where we last saw the player
        player = self.Owner( ).vis_spy
        assert( player is not None  ) # Sanity check
        self.player_pos = pos.Pos( player.location )
        # Set up the player pointer:
        self.spy = self.Owner( ).vis_spy
        assert( self.spy is not None ) # Sanity check
        # We keep a timeout so that if the player hides behind
        # a building, we don't immediately snap out of combat:
        self.vis_timeout = VIS_TIMEOUT
        # We just chase and shoot:
        self.PushAction( SHOOT( ) )
        self.PushAction( CHASE( ) )
        # SET THE ACTION SPRITE
        self.parent.spr_action = self.parent.SetSprite( io.SPRITE( "pursuing_state.gif", 0 ) )
    def Leave( self ):
        # CLEAR THE ACTION SPRITE
        self.parent.spr_action = self.parent.ClearSprite( self.parent.spr_action )
        # Forget the player:
        self.spy = None
        self.player_pos = None
        # Clear all our actions:
        self.ClearActions( )
    def Tick( self, currTime, deltaTime ):
        # If the guard can see the player, update our records:
        owner = self.Owner( )
        if owner.vis_spy is not None:
            # Update records:
            self.player_pos = pos.Pos( owner.vis_spy.location )
        # If we can't see the player, timeout:
        if owner.vis_spy is None:
            self.vis_timeout -= deltaTime
            # if we've timed out, forget the player:
            if self.vis_timeout <= 0:
                self.spy = None
        else:
            self.vis_timeout = VIS_TIMEOUT
        # Chain up for subactions:
        gob.ACTION.Tick( self, currTime, deltaTime )
    def Priority( self ):
        # If we've forgotten about (or never knew) the spy, we don't prioritise:
        if self.spy is None and self.Owner( ).vis_spy is None:
            # No spy in sight - we don't engage at all:
            return NO_NEED
        # What is the player needing now?
        security = self.parent.d_security
        strength = self.parent.d_strength
        # What will he need when we're done?
        security += ENGAGE_SEC
        strength += ENGAGE_STR
        # Discontentment is the hypotenuse of the others:
        discontent = math.hypot(security,strength)
        return security*security + strength*strength + discontent*discontent

# Disarm.
class DISARM (gob.ACTION):
    def Enter( self ):
        self.best_bomb = None
        # SET THE ACTION SPRITE
        self.parent.spr_action = self.parent.SetSprite( io.SPRITE( "defusing_state.gif", 0 ) )
    def Leave( self ):
        # Clear the action list:
        while len(self.actionList) > 0:
            self.actionList.pop( )
        # CLEAR THE ACTION SPRITE
        self.parent.spr_action = self.parent.ClearSprite( self.parent.spr_action )
    def Tick( self, currTime, deltaTime ):
        # Find the best bomb to defuse, calculated as the bomb we will reach
        # with the most time left.
        if self.best_bomb is None or self.best_bomb.deleted:
            self.best_bomb = self.BestBomb( )
            if self.best_bomb is not None:
                # We move to the bomb, then defuse it:
                self.PushAction( DEFUSEBOMB( self.best_bomb ) )
                self.PushAction( TRAVERSEEDGE( self.best_bomb.location ) )
                self.best_bomb.defuser = self
        # Chain up:
        gob.ACTION.Tick( self, currTime, deltaTime )
    def Priority( self ):
        # Disarm is only a priority if we can see a bomb:
        best_bomb = self.BestBomb( )
        if best_bomb is None:
            return NO_NEED
        # What is the player needing now?
        security = self.parent.d_security
        strength = self.parent.d_strength
        # What will he need when we're done?
        security += DISARM_SEC
        strength += DISARM_STR
        # Discontentment is the hypotenuse of the others:
        discontent = math.hypot(security,strength)
        return security*security + strength*strength + discontent*discontent
    def BestBomb( self ):
        # Returns the object of the best bomb, or None if we can't disarm any.
        vis_bombs = self.Owner( ).vis_bombs
        best_time = 0
        best_bomb = None
        for bomb in vis_bombs:
            if bomb.defuser is not None \
               and bomb.defuser is not self: continue
            curr_time = bomb.countdown + BOMB_DISARM_TIME \
                      - (bomb.location - self.Location( )).length( ) / DEFAULT_SPEED 
            if (best_time < curr_time):
                best_bomb = bomb
                best_time = curr_time
        return best_bomb

# FindPatrolDest.  Decides where on the map to patrol, then plans a path.
class FINDPATROLDEST (gob.ACTION):
    def Enter( self ):
        # Ask the map for the location with the longest time since patrol,
        # weighted by the distance from us:
        end = utility.MapToScreen( gamemap.MAP( ).WeightedDest( self.Location( ) ) )
        # Think about a path there:
        self.path = None
        self.PushAction( THINK( self.Location( ), end ) )
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        gob.ACTION.Tick( self, currTime, deltaTime )
        if len(self.actionList) == 0:
            self.parent.path = self.path
            self.parent.PopAction( )
            return

# FindHealthPoint.  Finds a healing point then plans a path.
class FINDHEALTHPOINT (gob.ACTION):
    def Enter( self ):
        # Ask the map for the most suitable healing building:
        owner = self.Owner( )
        building = gamemap.MAP( ).BestHealStation( owner )
        self.parent.healthpoint = building
        end = utility.MapToScreen( building.ClosestPoint( owner ) )
        # Think about a path there:
        self.path = None
        self.PushAction( THINK( self.Location( ), end ) )
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        gob.ACTION.Tick( self, currTime, deltaTime )
        if len(self.actionList) == 0:
            self.parent.path = self.path
            self.parent.PopAction( )
            return

# Think. Plans a path using Dijkstra's Algo., postponing
#        if the search is taking too long.
class THINK (gob.ACTION):
    
    def __init__( self, *args ):
        if len(args) is not 2: #expect: start, end vectors
            print "Wrong args in THINK init"
            raise SystemExit
        self.start = utility.ScreenToMap( args[0] )
        self.end = utility.ScreenToMap( args[1] )
    def Enter( self ):
        # Make a copy of the costs array:
        self.costs = gamemap.MAP( ).CostsClone( )
        # Initialise the A* search:
        self.search = astar.AStar( astar.SQ_MapHandler( \
            self.costs, \
            MAP_SIZE[0],MAP_SIZE[1]) )
        # Begin searching:
        self.search.StartSearch( self.start, self.end )

    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        # Perform the search:
        path = self.search.ContinueSearch( currTime, A_STAR_TIMELIMIT)

        # If we've found a path, we're done:
        if path is not None:
            ret_path = []
            ret_path.append(utility.MapToScreen( self.start ) )
            for n in path.nodes:
                ret_path.append(utility.MapToScreen( n.location ) )
            ret_path.append( utility.MapToScreen( self.end ) )
            self.parent.path = ret_path
            self.parent.PopAction( )
            # Empty the list so garbage collection has it easier:
            while len(self.costs): self.costs.pop( )
            return
        # If we've not yet found one, check if we've failed:
        else:
            if self.search.failed:
                print "A* search failed!"
                self.parent.PopAction( )
                # Empty the list so garbage collection has it easier:
                while len(self.costs): self.costs.pop( )
                return
       
# UseHealthPoint.  This guard will heal at the location travelled to.
class USEHEALTHPOINT (gob.ACTION):
    def Enter( self ):
        pass
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        # Change the guard's health and security desires:
        owner = self.Owner( )
        owner.d_strength += self.parent.healthpoint.heal_reward
        owner.d_security += self.parent.healthpoint.security_reward
        # Chain up:
        gob.ACTION.Tick( self, currTime, deltaTime )
        # Finished! Pop:
        self.parent.PopAction( )

# DefuseBomb.  Guards who are adjacent to bombs can disarm them.
class DEFUSEBOMB (gob.ACTION):
    def __init__( self, *args ):
        assert len(args) == 1 # expect: bomb
        self.bomb = args[0]
    def Enter( self ):
        self.countdown = BOMB_DISARM_TIME
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        self.countdown -= deltaTime
        if self.countdown <= 0:
            self.bomb.Destroy( )
            self.parent.PopAction( )
            return

# LookAround.  A guard looks around with greater radius while stationary.
class LOOKAROUND (gob.ACTION):
    def Enter( self ):
        self.Countdown = GUARD_LOOK_TIME
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        gob.ACTION.Tick( self, currTime, deltaTime )
        self.Countdown -= deltaTime
        if self.Countdown <= 0:
            self.parent.PopAction( )
            return

# Shoot.  Fires at an enemy.  Takes a very short while.
class SHOOT (gob.ACTION):
    def Enter( self ):
        self.bHasFired = False
        self.FireDelay = 0
    def Leave( self ):
        self.bHasFired = False
    def Tick( self, currTime, deltaTime ):
        if self.bHasFired == False:
            owner = self.Owner( )
            if owner.vis_spy is not None:
                owner.ShootRel( (owner.vis_spy.location - owner.location).normalise( ) \
                                * BULLET_RANGE )
        self.FireDelay += deltaTime
        if self.FireDelay > GUARD_RELOAD:
            self.bHasFired = False
            self.FireDelay = 0
        else:
            self.bHasFired = True
    def Priority( self ):
        if self.bHasFired == False:
            owner = self.Owner( )
            if owner.vis_spy is None:
                # Only priority if we can see the spy
                return 11
            # We've not fired, or we've fired and reloaded.
            # Higher priority if we're wounded.
            if owner.d_strength > owner.d_security:
                return 1
            return (owner.d_security - DESIRE_MIN) \
                   / max(owner.d_security - (owner.d_strength - DESIRE_MIN), 1) \
                   * 8 + 1
        else:
            # We've just fired.  We're priority until reloaded:
            return 0

# Chase. Closes on a distant enemy.
class CHASE (gob.ACTION):
    def __init__( self, *args ):
        # Chain up:
        gob.ACTION.__init__( self, args )
        # All chasers initially want to follow the player:
        self.try_to_follow = True
    def Enter( self ):
        pass
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        # If we're further than firing range, or we can't see the player,
        # close on the player's last good location:
        owner = self.Owner( )
        if owner.vis_spy is None \
           or (self.parent.player_pos - owner.location).length( ) >= CLOSE_RANGE:
            owner.MoveTo( self.parent.player_pos, deltaTime )
            # If we can't see the player and we're near their location, forget them:
            if owner.vis_spy is None and self.parent.player_pos is not None:
                if ( self.parent.player_pos - owner.location ).length( ) \
                   < MOVE_CLOSE_ENOUGH:
                    self.try_to_follow = False
            elif owner.vis_spy is not None:
                # We can see the player, so we still try to follow:
                self.try_to_follow = True
        # Chain up:
        gob.ACTION.Tick( self, currTime, deltaTime )
    def Priority( self ):
        # If we have an idea for the player's loc, we know where to go:
        if self.parent.player_pos is not None:
            if self.Owner( ).vis_spy is None:
                # No spy visible but we have an idea of where, so chase:
                if self.try_to_follow:
                    self.parent.vis_timeout = VIS_TIMEOUT
                return 0
            player_pos = self.parent.player_pos
        else:
        # If we haven't foung a player yet, calc one:
            vis_spy = self.Owner( ).vis_spy
            if vis_spy is None:
                # No visible spy, no idea where: we're not important.
                return 10
            # Visible spy, so set our player point:
            player_pos = vis_spy.location
        # Priority is 0 if we're out of bullet range, and increases to 10
        # as we move from bullet range to close range.
        dist = (player_pos - self.Owner( ).location).length( )
        if dist > BULLET_RANGE:
            return 0
        if dist <= CLOSE_RANGE:
            return 10
        return (BULLET_RANGE - CLOSE_RANGE) \
               / ( BULLET_RANGE - (dist - CLOSE_RANGE) ) * 10

# Goto.  Follows a path made using Dijkstra's Algo.; makes
#        use of TRAVERSEEDGE actions to get there.
class GOTO (gob.ACTION):
    def Enter( self ):
        if self.parent.path:
            # Go through parent's 'path' list and make all our TraverseEdges:
            path = list( )
            for loc in self.parent.path:
                path.append( loc )
            while len(path) > 0:
                self.PushAction( TRAVERSEEDGE(path.pop( )) )
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        gob.ACTION.Tick( self, currTime, deltaTime )
        if len(self.actionList) == 0:
            self.parent.PopAction( )
            return
    
# TraverseEdge.  Walks a straight line.
class TRAVERSEEDGE (gob.ACTION):
    def __init__( self, *args ):
        if len(args) is not 1: # expect: target location as tuple or vector
            print "TRAVERSEEDGE passed wrong number of parameters."
            raise SystemExit
        # Set the target location:
        self.target = pos.Pos(args[0])
    def Enter( self ):
        pass
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        # Move towards target:
        self.MoveTo( self.target, deltaTime )
        # If we're at the target, stop moving:
        location = self.Location( )
        if abs(location.x - self.target.x) < MOVE_CLOSE_ENOUGH and \
           abs(location.y - self.target.y) < MOVE_CLOSE_ENOUGH:
            self.parent.PopAction( )

####################################################
# GENERIC actions. These can be used by any objects.

# Idle.  Does nothing.
class IDLE (gob.ACTION):
    def Enter( self ):
        pass
    def Leave( self ):
        pass
    def Tick( self, currTime, deltaTime ):
        pass
